package com.googlecode.cswish.struts.spring;

import java.lang.reflect.Array;
import java.util.Map;

import javax.annotation.Resource;

import org.apache.commons.lang.ClassUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.UnsatisfiedDependencyException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.stereotype.Service;

import com.googlecode.cswish.Config;
import com.googlecode.cswish.model.ModelInfo;
import com.googlecode.cswish.util.ClassUtil;
import com.opensymphony.xwork2.inject.Container;
import com.opensymphony.xwork2.util.ClassLoaderUtil;

/**
 * The core class to implement the functions of cswish:
 * <p>
 *  <li> Load spring configuration if developer doesn't register it in web.xml
 *  <li> Mock a structs action if developer doesn't create it (CSwishUnknownHandler adds it to struts configuration)
 *  <li> Process some start-up class
 *  <li> Build a new instance or get a singleton instance 
 * <p>
 * FIXME: the override class definition can be only defined in file "applicationContext-*.xml" and don't let spring container knows these files
 * (the ObjectFactory class rewrites the spring loader mechanism)
 * 
 * <p>Autowire class instance
 * 	 
 * @author Jerry.feng Date: 27/08/2008
 */
@Service
public class ObjectFactory {
	
	private static final Log logger = LogFactory.getLog(ObjectFactory.class);
	
	private static volatile ObjectFactory instance;
	
	private int autowireStrategy = AutowireCapableBeanFactory.AUTOWIRE_BY_NAME;
	private boolean alwaysRespectAutowireStrategy = false;
	
	private BeanIntrospector beanIntrospector;
	
	private Container container;
	
	private ApplicationContext appContext;
	 protected AutowireCapableBeanFactory autoWiringFactory;
	
	@Autowired
	@Required
	public void setBeanFactory(ApplicationContext appContext) {
		this.appContext = SiteApplicationContext.getInstance(appContext);
        this.autoWiringFactory = findAutoWiringBeanFactory(this.appContext);
	}
	
	private AutowireCapableBeanFactory findAutoWiringBeanFactory(ApplicationContext context) {
        if (context instanceof AutowireCapableBeanFactory) {
            // Check the context
            return (AutowireCapableBeanFactory) context;
        } else if (context instanceof ConfigurableApplicationContext) {
            // Try and grab the beanFactory
            return ((ConfigurableApplicationContext) context).getBeanFactory();
        } else if (context.getParent() != null) {
            // And if all else fails, try again with the parent context
            return findAutoWiringBeanFactory(context.getParent());
        }
        return null;
    }
	
	@Resource
	private Config config;
	
	private BeanIntrospector getBeanIntrospector() {
		if (this.beanIntrospector == null) {
			this.beanIntrospector = getBean(BeanIntrospector.class, false);
		}
		
		return this.beanIntrospector;
	}
	
	/**
	 * Provide a singleton reference.<br>
	 * Create a new factory temporarily if the factory is not prepared.
	 * 
	 * @return
	 */
	public static ObjectFactory getInstance() {
		if (instance != null) {
			return instance;
		}
		
		synchronized (ObjectFactory.class) {
			if (instance != null) {
				return instance;
			}
			
			// for application mode
			StrutsObjectFactory strutsObjectFactory = new StrutsObjectFactory(null, null, null);
			instance = strutsObjectFactory.getBean(ObjectFactory.class, false);
			return instance;
		}
	}
	
	public ObjectFactory() {
		instance = this;
	}

	public Container getContainer() {
		return this.container;
	}
	
	void setContainer(Container container) {
		this.container = container;
	}
	
	public void refreshSpringContext() {
		if (appContext instanceof SiteApplicationContext) {
			SiteApplicationContext siteApplicationContext = (SiteApplicationContext)appContext;
			siteApplicationContext.refresh();
		}
	}
	
	public Object injectInternalBeans(Object obj) {
		if (obj != null && container != null) {
            container.inject(obj);
        }
		return obj;
	}
	
	public void register(String name, Class<?> clazz, boolean isSingleton) throws BeansException {
		GenericBeanDefinition bd = new GenericBeanDefinition();
		bd.setBeanClass(clazz);
		
		javax.annotation.Resource resource = clazz.getAnnotation(javax.annotation.Resource.class);
		if (resource != null) {
			bd.setScope(resource.shareable() ? BeanDefinition.SCOPE_SINGLETON : BeanDefinition.SCOPE_PROTOTYPE);
		} else {
			bd.setScope(isSingleton ? BeanDefinition.SCOPE_SINGLETON : BeanDefinition.SCOPE_PROTOTYPE);
		}
		String defaultName = getDefaultBeanName(name);
		findRegistry(autoWiringFactory).registerBeanDefinition(name, bd);
		findRegistry(autoWiringFactory).registerAlias(name, defaultName);
	}
	
	protected BeanDefinitionRegistry findRegistry(AutowireCapableBeanFactory autoWiringFactory) {
		return (BeanDefinitionRegistry) autoWiringFactory;
	}
	
	public boolean containsBeanDefinition(String className) {
		if (autoWiringFactory instanceof BeanDefinitionRegistry) {
			BeanDefinitionRegistry registry = (BeanDefinitionRegistry) autoWiringFactory;
			return registry.containsBeanDefinition(className);
		} else {
			return false;
		}
	}
	
	public BeanDefinition getBeanDefinition(String className) {
		if (autoWiringFactory instanceof BeanDefinitionRegistry) {
			BeanDefinitionRegistry registry = (BeanDefinitionRegistry) autoWiringFactory;
			return registry.getBeanDefinition(className);
		}
		return null;
	}
	
	public <T> T getBean(Class<T> type, boolean autowireAndInject) {
		String[] names = this.appContext.getBeanNamesForType(type);
		String name;
		if (names == null || names.length == 0) {
			// register the bean definition temporarily
			if (config != null) {
				String implName = config.getClassImplName(type.getName());
				if (implName != null) {
					try {
						type = ClassUtils.getClass(implName);
					} catch (ClassNotFoundException ex) {
						logger.error("Failed to register bean: " + implName, ex);
					}
				}
			}
			
			if (type.isInterface()) {
				return null;
			}
			register(type.getName(), type, true);
			name = type.getName();
		} else {
			name = names[names.length - 1];
		}
		Object bean = this.appContext.getBean(name);
		if (autowireAndInject) {
			autoWireBean(bean);
		}
		return (T) bean;
	}
	
	public Object autoWireBean(Object bean) {
        return autoWireBean(bean, autoWiringFactory);
    }
	
	/**
     * @param bean
     * @param autoWiringFactory
     */
    public Object autoWireBean(Object bean, AutowireCapableBeanFactory autoWiringFactory) {
        if (autoWiringFactory != null) {
            autoWiringFactory.autowireBeanProperties(bean,
                    autowireStrategy, false);
        }
        injectApplicationContext(bean);

        injectInternalBeans(bean);

        return bean;
    }
    
    private void injectApplicationContext(Object bean) {
        if (bean instanceof ApplicationContextAware) {
            ((ApplicationContextAware) bean).setApplicationContext(this.appContext);
        }
    }
	
	public <T> T[] getBeans(Class<T> type, boolean autowireAndInject) {
		String[] names = this.appContext.getBeanNamesForType(type);
		if (names == null || names.length == 0) {
			if (type.isInterface()) {
				return null;
			}
			
			// register the bean definition temporarily
			if (config != null) {
				String implName = config.getClassImplName(type.getName());
				if (implName != null) {
					try {
						type = ClassUtils.getClass(implName);
					} catch (ClassNotFoundException ex) {
						logger.error("Failed to register bean: " + implName, ex);
					}
				}
			}
			register(type.getName(), type, true);
			names = new String[] {type.getName()};
		}
		T[] beans = (T[]) Array.newInstance(type, names.length);
		for (int i = 0; i < names.length; i++) {
			String name = names[i];
			T bean = (T) this.appContext.getBean(name);
			if (autowireAndInject) {
				autoWireBean(bean);
			}
			beans[i] = bean;
		}
		return (T[]) beans;
	}
	
	public Object getBean(String className, boolean autowireAndInject) {
		ModelInfo modelInfo = getBeanIntrospector().getModelInfo(className);
		if (modelInfo != null) {
			if (modelInfo.isDynamicClass()) {
				ClassLoader loader = getBeanIntrospector().dynamicClassLoaderFactory.getClassLoader(
						modelInfo.getType(), true);
				Thread.currentThread().setContextClassLoader(loader);
			}
		}
		
		Object bean;
		if (this.appContext.containsBeanDefinition(className)) {
			bean = this.appContext.getBean(className);
		} else {
			String defaultName = getDefaultBeanName(className);
			if (this.appContext.containsBeanDefinition(defaultName)) {
				bean = this.appContext.getBean(defaultName);
			} else {
				try {
					Class clazz = Class.forName(className, true, Thread.currentThread().getContextClassLoader());
					bean = this.appContext.getBean(clazz);
				} catch (ClassNotFoundException ex) {
					logger.debug("type is not valid class:" + className);
					bean = null;
				}
			}
		}
		if (autowireAndInject) {
			autoWireBean(bean);
		}
		return bean;
	}
	
	private String getDefaultBeanName(String className) {
		int index = className.lastIndexOf('.');
		String beanName;
		if (index > 0) {
			char c1 = className.charAt(index + 1);
			char c2 = className.charAt(index + 2);
			if (Character.isLowerCase(c1) || Character.isUpperCase(c2)) {
				beanName = className.substring(index + 1);
			} else {
				beanName = Character.toLowerCase(c1) + className.substring(index + 2, className.length());
			}
		} else {
			beanName = className;
		}
		return beanName;
	}
	
	/**
	 * Try 2 ways to create the model object, firstly, regard modelName as the 
	 * full class name to create model. secondly, regard modelName as the simple
	 * name to create model.
	 *  
	 * @param modelName
	 * @return
	 */
	public Object buildModel(String modelName, Map context) {
		if (modelName.indexOf('$') >= 0) {		// don't support create the inner class
			return null;
		}
		
		Object bean = null;
		ModelInfo modelInfo = getBeanIntrospector().getModelInfo(modelName);
		if (modelInfo != null) {
			try {
				if (modelInfo.isDynamicClass()) {
					ClassLoader loader = getBeanIntrospector().dynamicClassLoaderFactory.getClassLoader(
							modelInfo.getType(), true);
					Thread.currentThread().setContextClassLoader(loader);
				}
				Class clazz = modelInfo.getTypeClass();
				if (clazz != null && !clazz.isInterface()) {
					bean = clazz.newInstance();
				}
			} catch (Exception ex) {
				logger.error("Can't create model:" + modelInfo.getType(), ex);
			}
		}
		return bean;
	}
	
	///////////////////////////////////////////////////////////////////////////
	/**
	 * Create object with a little "AI":
	 * 
	 * <li>Request path is the object simple name
	 * <li>"model" means the mode object in the model package
	 * <li>"service" means the service object in the application package
	 * 
	 * 
	 * @param modelName
	 * @param destType
	 * @param injectInternal
	 * @return
	 */
	@SuppressWarnings("rawtypes")
	public Object buildBean(String modelName, Class destType, Map context) {
		if (destType == Object.class && modelName != null || getBeanIntrospector().isModel(modelName) >= 0) {
			String type = ClassUtil.getRuntimeType(modelName);
			if (type != null) {
				return buildModel(type, context);
			} else {
				logger.warn("Invalid runtime type: null");
				return null;
			}
		}
		
		try {
			if (getBeanIntrospector().isModel(destType.getName()) >= 0) {
				return buildModel(destType.getName(), context);
			} else {
				return buildBean(destType, context);
			}
		} catch (Exception ex) {
			logger.error("Can't create object:" + destType.getName(), ex);
		}
		return null;
	}
	
	/**
     * Build a generic Java object of the given type.
     *
     * @param className the type of Object to build
     * @param extraContext a Map of extra context which uses the same keys as the {@link com.opensymphony.xwork2.ActionContext}
     */
    public Object buildBean(String className, Map<String, Object> extraContext) throws Exception {
        return buildBean(className, extraContext, true);
    }
    
    /**
     * Build a generic Java object of the given type.
     *
     * @param className the type of Object to build
     * @param extraContext a Map of extra context which uses the same keys as the {@link com.opensymphony.xwork2.ActionContext}
     */
    public Object buildBean(String beanName, Map<String, Object> extraContext, boolean injectInternal) throws Exception {
        Object o;
        
        if (appContext.containsBean(beanName)) {
            o = appContext.getBean(beanName);
        } else {
            Class beanClazz = getClassInstance(beanName);
            o = buildBean(beanClazz, extraContext);
        }
        if (injectInternal) {
            injectInternalBeans(o);
        }
        return o;
    }
    
    public Class getClassInstance(String className) throws ClassNotFoundException {
        Class clazz;
        if (appContext.containsBean(className)) {
            clazz = appContext.getBean(className).getClass();
        } else {
            clazz = ClassLoaderUtil.loadClass(className, this.getClass());
        }
        return clazz;
    }
	
    /**
     * @param clazz
     * @param extraContext
     * @throws Exception
     */
    public Object buildBean(Class clazz, Map<String, Object> extraContext) throws Exception {
        Object bean;

        try {
            // Decide to follow autowire strategy or use the legacy approach which mixes injection strategies
            if (alwaysRespectAutowireStrategy) {
                // Leave the creation up to Spring
                bean = autoWiringFactory.createBean(clazz, autowireStrategy, false);
                injectApplicationContext(bean);
                return injectInternalBeans(bean);
            } else {
                bean = autoWiringFactory.autowire(clazz, AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false);
                bean = autoWiringFactory.applyBeanPostProcessorsBeforeInitialization(bean, bean.getClass().getName());
                // We don't need to call the init-method since one won't be registered.
                bean = autoWiringFactory.applyBeanPostProcessorsAfterInitialization(bean, bean.getClass().getName());
                return autoWireBean(bean, autoWiringFactory);
            }
        } catch (UnsatisfiedDependencyException e) {
            if (logger.isErrorEnabled())
            	logger.error("Error building bean", e);
            // Fall back
            return autoWireBean(clazz.newInstance(), autoWiringFactory);
        }
    }

	///////////////////////////////////////////////////////////////////////////

	public AutowireCapableBeanFactory getAutowireCapableBeanFactory() {
		return autoWiringFactory;
	}

	public static class ObjectFactoryBean implements FactoryBean {
		@Override
		public Object getObject() throws Exception {
			return ObjectFactory.getInstance();
		}

		@Override
		public Class getObjectType() {
			// TODO Auto-generated method stub
			return ObjectFactory.class;
		}

		@Override
		public boolean isSingleton() {
			return true;
		}
	}
}